<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8">
    <title>Sucha's Blog - Archive for January, 2024</title>
    <meta name="generator" content="MarkdownProjectCompositor.lua">
    <meta name="author" content="Sucha">
    <meta name="keywords" content="suchang, programming, Linux, Lua">
    <meta name="description" content="Sucha's blog">
    <link rel="shortcut icon" href="../images/ico.png">
    <link rel="stylesheet" type="text/css" href="../styles/blog.css">
    <link rel="stylesheet" type="text/css" href="../styles/prism.min.css">
    <style id="site_theme"></style>
  </head>
  <body>
    <div id="body">
      <div id="text">
	   <!-- Page published by cmark-gfm begins here --><h1>Sucha's Blog ~ Archive for January, 2024</h1>
<p><a id="p0"></a></p>
<div class="date">24年1月28日 周日 22:47</div>
<h2>WebSocket(1)</h2>
<p>研究了一下 WebSocket，在 HTTP/1.1 之上，提供了一个双工长连接，通过 header 的 Upgrade 来完成协议升级及参数交换，之后就都是 websocket 的 frame 了。其最基础的协议描述在 <a href="https://www.rfc-editor.org/rfc/rfc6455">RFC6455</a>，压缩部分的扩展在 <a href="https://www.rfc-editor.org/rfc/rfc7692">RFC7962</a>。</p>
<p>之前研究了一下 <a href="https://github.com/mortzdk/Websocket">C 写的 Websocket</a>，验证了一下是可以正常工作的，但它 frame 的解析部分不好单独摘出来。</p>
<p>client 端的 html + js websocket data send/recv 其实很好写，browser 本来就有 WebSocket 的接口给调用，拉起本地的一个 html 就行了，但是 browser 无法指定 http header 的参数（包括 ws extension），发送 text、binary 的 frame 时，是通过 js 的 string、uint8 array 来指定的。</p>
<p>想着找一个可以直接提供解析 websocket frame 的库，虽然找到了 <a href="https://github.com/tatsuhiro-t/wslay">C 写的 wslay</a>，但是实际使用过程中，感觉使用实在是繁琐。</p>
<p>比如 websocket frame 的结构如下：</p>
<pre><code class="language-js"> 0               1               2               3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

</code></pre>
<ul>
<li>header 长度至少 2 bytes，FIN 代表是最后一帧，RSV 1 ~ 3 是保留用，用于描述 extension 的使用</li>
<li>opcode 4 bits 描述了 PING、PONG、CLOSE、TEXT、BINARY、CONTINUATION 这几种 frame type</li>
<li>mask 1 bit，只有在客户端发送给服务端的时候需要设置 1，激活之后的 masking-key 字段</li>
<li>payload len 至少 7 bits, &lt;= 125 为当前 frame 长度，126 意味着使用接下来的 payload len 长度 16 bits，127 则是 64 bits</li>
<li>masking-key 是当 mask 为 1 时，client 发送给 server 的数据需要跟 masking-key 4 bytes 做异或运算，masking-key 则是 client 自己提供的</li>
<li>之后就是 payload len 指定长度的 payload data 数据了</li>
</ul>
<p>回到之前说的 wslay，霓虹国写的东西太复杂，居然 header 没检测全都能返回好几种错误，我感觉这部分其实可以简单点，比如 header 都解析不了的时候其实可以提示数据不足（毕竟 header 数据都不足），缺点是放弃了一部分错误的检查。</p>
<p>在 HTTP/1.1 的协议升级到 websocket 之前，header 需要交换一些数据，比如 client 会发送如下的字段</p>
<pre><code class="language-js">Sec-WebSocket-Version: 13
Sec-WebSocket-Key: fSQMpewdgSIz1IhuhL3ERQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
</code></pre>
<ul>
<li>Sec-WebSocket-Version 固定是 13，其他的版本貌似都是实验性质的</li>
<li>Sec-WebSocket-Key 是 client 对 server 的校验吧，server 拿到这个值后，需要跟 &quot;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&quot; 结合做 SHA1 散列，base64 后放到返回 HTTP/1.1 协议升级结果的 header 中</li>
<li>Sec-WebSocket-Extensions 是 websocket 的扩展描述，上面使用了 permessage-deflate，对每一个消息都使用 deflate 算法做压缩，client_max_window_bits 应该是参数吧，不大懂</li>
</ul>
<p>说到 permessage-deflate，server 在 deflate payload 后构建 frame 时，需要设置 rsv1 bit 为 1，要不 client 会忽略 extensions 的描述。</p>
<p>还有 server 在 inflate frame payload 时，实际上认为不断喂进来的 payload 是连续的 data stream，因此不需要做 inflateEnd。</p>
<p>这个感觉也跟 HTTP/1.1 的实际处理类似，实际每个 http requet / response 都是一个单独的 inflate / deflate stream，websocket 升级了协议后，可以认为不断发送过来（出去）的 payload 是没有结束的 http body 数据，因此不需要结束这个 inflate / delfate stream。</p>
<p>其他比如 ping、pong、close frame 类型没怎么研究，不过感觉 close 没啥作用，也许是用于实际关闭 tcp 链接前的一个处理吧，也许给调用层处理，在 websocket frame 这一层感觉没什么必要，毕竟无论如何总是要处理没有发送 close 的资源注销的，那又何必多此一举要这个 close 呢。</p>
<div class="category"><a href="CategoryProgramming.html">CategoryProgramming</a> / <a href="2024-01.html#p0">Permalink</a> / <a href="https://github.com/lalawue/homepage/discussions/categories/blog" target="_blank">Discussion</a></div>
<!-- Page published by cmark-gfm ends here -->
  <div id="foot">2004-<script>var d = new
	Date();document.write(d.getFullYear())</script> &copy;
	Sucha. Powered by MarkdownProjectCompositor.
  </div>
  </div><!-- text -->
  <div id="sidebar">
  </div><!-- sidebar -->
  <script src="../js/prism.min.js" async="async"></script>
  <script src="../js/blog_sidebar.js"></script>
  </div> <!-- body -->
</body>
</html>